package org.apache.ibatis.config.binding;

import org.apache.ibatis.common.io.ResolverUtil;
import org.apache.ibatis.sql.session.Configuration;
import org.apache.ibatis.sql.session.SqlSession;

import java.util.*;

/**
 * @Name MapperRegistry
 * @Description: [{映射接口:抽象方法 }-> {映射文件:数据库操作节点}] 注册表
 * 问题：
 * 一方面，映射接口文件（UserMapper.class等存有接口的文件）那么多，其中的抽象方法又很多；、
 * 另一方面，映射文件（UserMapper.xml等存有  SQL操作语句的文件）那么多，映射文件中的数据库操作节点又很多，那么这一切的对应关系怎么维护呢？
 * 也就是说，一个映射接口中的抽象方法如何确定自身要接入的 MapperMethod对象是哪一个呢？
 * MyBatis 分两步解决了这个问题：
 * 第一步，使用 MapperRegistry 注册表将映射接口与 MapperProxyFactory 关联起来；MapperProxyFactory 工厂除了映射接口之外的其他属性都是不可变的，产生出来的 MapperProxy 对象（映射接口实现类）也便确定了，相当于将映射接口和 MapperProxy 对象关联起来了。而 MapperProxy 对应的是接口映射文件，也相当于将映射接口和接口映射文件的对应关系建立起来了。
 * 第二步：使用 MapperProxy 对象中的 methodCache 属性维护抽象方法与 MapperMethod 对象（数据库操作节点）的对应关系。
 * 此外，MapperRegistry 注册表将被 Configuration 持有，方便查询。
 * @Version v1.0.0
 * @Author: lichao
 * @Date: 2024-05-16 16:45:09
 **/
public class MapperRegistry {
    private final Configuration config;
    // <映射接口，MapperProxy 工厂> 缓存，记录 Mapper 映射接口与 MapperProxy 对象之间的绑定关系
    private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();

    public MapperRegistry(Configuration config) {
        this.config = config;
    }

    /**
     * 将映射接口添加到注册表中。
     *
     * @version v1.0.0
     * @author lichao
     * @date 2024-05-17 15:34:34
     */
    public <T> void addMapper(Class<T> type) {
        if (type.isInterface()) {
            if (hasMapper(type)) {
                String msg = "映射接口 [" + type + "] 已经存在于注册表中了，不允许重复添加。";
                throw new BindingException(msg);
            }
            boolean loadCompleted = false;
            try {
                knownMappers.put(type, new MapperProxyFactory<>(type));
                // 在运行解析器之前添加类型是很重要的。否则，可以由自动尝试绑定映射器解析器。如果类型已知，则不会尝试。
                MappingAnnotationBuilder parser = new MappingAnnotationBuilder(config, type);
                parser.parse();
                loadCompleted = true;
            } finally {
                if (!loadCompleted) knownMappers.remove(type);
            }
        }
    }

    /**
     * 找到指定映射接口的映射文件，并根据映射文件信息为该映射接口生成一个代理实现。
     *
     * @version v1.0.0
     * @author lichao
     * @date 2024-05-17 15:17:25
     */
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        final MapperProxyFactory<T> factory = (MapperProxyFactory<T>) knownMappers.get(type);
        if (factory == null) {
            String msg = "MapperRegistry 注册表中不存在映射接口 [" + type + "]。";
            throw new BindingException(msg);
        }
        try {
            return factory.newInstance(sqlSession);
        } catch (Exception e) {
            String msg = "无法获取映射接口的代理实例，原因是：" + e;
            throw new BindingException(msg, e);
        }
    }

    /**
     * 判断注册表中是否存在指定的映射接口
     *
     * @version v1.0.0
     * @author lichao
     * @date 2024-05-17 15:26:43
     */
    public <T> boolean hasMapper(Class<T> type) {
        return knownMappers.containsKey(type);
    }

    public void addMappers(String packageName) {
        addMappers(packageName, Object.class);
    }

    public void addMappers(String packageName, Class<?> superType) {
        ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
        // 筛选出某个包下 Object 的所有子类，即所有类；
        resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
        // 找到符合条件的类集合
        Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
        // 将它们添加到注册表中
        for (Class<? extends Class<?>> mapperClass : mapperSet) {
            addMapper(mapperClass);
        }
    }

    /**
     * 获取注册表中所有的映射接口
     *
     * @version v1.0.0
     * @author lichao
     * @date 2024-05-17 15:26:26
     */
    public Collection<Class<?>> getMappers() {
        return Collections.unmodifiableCollection(knownMappers.keySet());
    }

}

